package com.nhncorp.pleiades.master;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import org.jgroups.ChannelException;
import org.jgroups.JChannel;
import org.jgroups.Message;

import com.nhncorp.pleiades.conf.PleiadesConfiguration;
import com.nhncorp.pleiades.util.Sleeper;

public class ACLController implements Runnable, Controller{

  private JChannel clusterChannel = null;
  protected final AtomicBoolean stopRequested = new AtomicBoolean(false);
  protected final AtomicBoolean reloadAclTable = new AtomicBoolean(true);
  private Sleeper sleeper;
  private PleiadesConfiguration conf;
  private int refreshInterval;
  SAXBuilder builder = null;
  Document document = null;  
  private Map<String, ServiceACL> serviceAccessTable;
  Set<String> serviceIdSets = null;
  File serviceAclFile = null;
  String serviceAclFileName;
  Object serviceTableLock = null;
  Object serviceMapLock = null;
  
  public static final Log LOG = LogFactory.getLog(ACLController.class);

  public ACLController(JChannel channel, PleiadesConfiguration conf) {
    this.clusterChannel = channel;
    this.conf = conf;
    this.sleeper = new Sleeper(1000, this.stopRequested);
    this.serviceAccessTable = new ConcurrentHashMap<String, ServiceACL>();
    this.serviceIdSets = new TreeSet<String>();
    this.serviceAclFileName = conf.get("clusterManager.acl.filename", "service_acl.xml");
    File aclDirectory = new File(conf.get("clusterManager.acl.dir", "conf"));
    if( aclDirectory.exists() ) {
      Long versionNum = new Long(0);
      String[] tempFileNames = aclDirectory.list();
      if(tempFileNames != null) {
        for(String tempFileName : tempFileNames) {
          if(tempFileName.startsWith("service_acl.xml")) {
            int index = tempFileName.indexOf("xml_");
            if(index >= 0) {
              String number = tempFileName.substring(index+4);
              if(versionNum < new Long(number)) {
                this.serviceAclFileName = tempFileName;
              }
            }
          }
        }
      }
    }
    this.serviceAclFile = new File(conf.get("clusterManager.acl.dir", "conf") + "/" + this.serviceAclFileName);
    this.refreshInterval = conf.getInt("clusterManager.refreshInterval", 30 * 1000);
    this.builder = new SAXBuilder();
    this.serviceTableLock = new Object();
    this.serviceMapLock = new Object();
  }
  
  public boolean updateServiceTable() throws IOException {
    boolean isUpdated = false;
    if(reloadAclTable.get()) {
      loadServiceTableFromFile();
      getAllPathsForService();
      if(clusterChannel.getView().getCreator().equals(clusterChannel.getLocalAddress())) {
        sendServiceTable();  
      }
      isUpdated = true;
    }
    if(isUpdated) {
      reloadAclTable.set(false);
    }
    return isUpdated;
  }
  
  public void storeServiceTable(ServiceACL[] aclArray) throws IOException {
    synchronized (serviceTableLock) {
      LOG.fatal("storeServiceTable() is called");
      this.serviceAclFile = new File(conf.get("clusterManager.acl.dir", "conf")
          + "/" + conf.get("clusterManager.acl.filename", "service_acl.xml")
          + "_" + System.nanoTime());
      writeServiceTable(aclArray);
      reloadAclTable.set(true);
      updateServiceTable();
    }
  }
  
  public void spreadServiceTable() throws IOException {
    try {
      if( !updateServiceTable() ) {
        sendServiceTable();  
      } 
    } catch (Exception e) {
      throw new IOException(e.getMessage());
    }
  }
  
  public String getPathForService(String clientIp, String serviceName)
      throws IOException {
    ServiceACL serviceACL = null;
    synchronized (serviceTableLock) {
      serviceACL = this.serviceAccessTable.get(serviceName);
    }
    if (serviceACL != null) {
      for (String addressInTable : serviceACL.getIpAddressList()) {
        if (checkIpAddress(clientIp, addressInTable)) {
          return serviceACL.getServiceId();
        }
      }
    } else {
      LOG.fatal(serviceName + " does not exist in the ACL Table");
    }
    return null;
  }

  
  public Set<String> getAllPathsForService() {
    Map<String, ServiceACL> serviceToPathMaps = new HashMap<String, ServiceACL>();
    if(this.serviceAccessTable != null & !this.serviceAccessTable.isEmpty()) {
      synchronized(serviceTableLock) {
        serviceToPathMaps.putAll(this.serviceAccessTable);  
      }
    }
    synchronized (serviceMapLock) {
      this.serviceIdSets.clear();
      for (Map.Entry<String, ServiceACL> entry : serviceToPathMaps.entrySet()) {
        this.serviceIdSets.add(entry.getValue().getServiceId());
      }
    }
    return this.serviceIdSets;
  }
  
  public boolean isValidateLock(String lockId) {
    
    boolean isSuccess = false;    
    synchronized (serviceMapLock) {
      if(this.serviceIdSets.contains(lockId)) {
        return isSuccess;
      } else {
        for(String id : this.serviceIdSets) {
          if(lockId.startsWith(id + "/")) {
            isSuccess = true;
            break;
          }
        }       
      }
    }
    return isSuccess;
  }
  
  private void loadServiceTableFromFile() throws IOException {    
    try {
      if(document != null) {
        document.removeContent();
      }
      synchronized(serviceTableLock) {
        if(this.serviceAccessTable != null && !this.serviceAccessTable.isEmpty()) {
          this.serviceAccessTable.clear();  
        }
        document = builder.build(this.serviceAclFile);
        Element rootElement = document.getRootElement();
        List<Element> serviceList = rootElement.getChildren();
        for(Element serviceElement : serviceList) {
          Attribute attr = serviceElement.getAttribute("id");
          ServiceACL serviceACL = new ServiceACL(serviceElement.getName(), attr.getValue());
          List<Element> ipElementList = serviceElement.getChildren();
          List<String> ipAddressList = new ArrayList<String>();
          for(Element address : ipElementList) {
            ipAddressList.add(address.getText());
          }
          serviceACL.setIpAddressList(ipAddressList);
          serviceAccessTable.put(serviceElement.getName().toString(), serviceACL);
        }
      }
    } catch (JDOMException e) {
      throw new IOException(e.getMessage());
    } 
  }
  
  private void sendServiceTable() throws IOException {
    try {
      Map<String, ServiceACL> copiedServiceAccessTable = new HashMap<String, ServiceACL>();
      synchronized(serviceTableLock) {
        copiedServiceAccessTable.putAll(serviceAccessTable);  
      }
      int index = 0;
      ServiceACL[] acls = new ServiceACL[copiedServiceAccessTable.size()];
      for(Map.Entry<String, ServiceACL> entryACL: copiedServiceAccessTable.entrySet()) {
        acls[index] = entryACL.getValue();
        ++index;
      }
      clusterChannel.send(new Message(null, null, acls));
      copiedServiceAccessTable.clear();
      copiedServiceAccessTable = null;        
    } catch (ChannelException e) {
      throw new IOException(e.getMessage());
    }
  }
  
  private void writeServiceTable(ServiceACL[] aclArray) throws IOException {
    Element rootElement = new Element("service");
    Document document = new Document(rootElement);    
    for(ServiceACL serviceAcl : aclArray) {
      Element serviceNameElement = new Element(serviceAcl.getType());
      serviceNameElement.setAttribute(new Attribute("id", serviceAcl.getServiceId()));
      List<String> ipList = serviceAcl.getIpAddressList();
      for(String ipAddr : ipList) {
        Element ipElement = new Element("ip");
        ipElement.addContent(ipAddr);
        serviceNameElement.addContent(ipElement);
      }
      rootElement.addContent(serviceNameElement);
    }    
    XMLOutputter outputter = new XMLOutputter();
    FileWriter writer = new FileWriter(this.serviceAclFile);
    outputter.output(document, writer);
    writer.flush();
    writer.close();
  }

  private boolean checkIpAddress(String clientIp, String authorizedIp) throws IOException {
    if(authorizedIp.indexOf('/') > 0) {
      if( clientIp.startsWith(processSubnetMask(authorizedIp)) ) {
        return true;
      }
    } else {
      if( clientIp.equals(authorizedIp)) {
        return true;
      }
    }
    return false;
  }
  
  private String processSubnetMask(String ipAddress) throws IOException {
    int subnetMaskPos = ipAddress.indexOf('/');
    String strMaskNumber = ipAddress.substring(subnetMaskPos+1);
    int maskNumber = 0;
    if( (maskNumber=Integer.parseInt(strMaskNumber)) == 8) {
      ipAddress = ipAddress.substring(0, ipAddress.indexOf('.'));
    } else if(maskNumber == 16) {
      ipAddress = ipAddress.substring(0, ipAddress.lastIndexOf('.'));
      ipAddress = ipAddress.substring(0, ipAddress.lastIndexOf('.'));
    } else if(maskNumber == 24) {
      ipAddress = ipAddress.substring(0, ipAddress.lastIndexOf('.'));
    } else {
      throw new IOException("Wrong Subnet Mask Setting");      
    }
    return ipAddress;
  }
  
  public void shutdown() {
    this.stopRequested.set(true);
  }
  
  public void run() {
    LOG.info("----------- Service Access Control Daemon Starts ----------");
    try {
      loadServiceTableFromFile();
      refreshLoop();
    } catch (IOException e) {
      e.printStackTrace();
    }    
  }
  
  private void refreshLoop() throws IOException {
    while (!stopRequested.get()) {
      updateServiceTable();
      sleeper.sleepMilliSeconds(refreshInterval);
    }
  }

}
